package com.gitee.l0km.codegen.decorator;
import static java.lang.String.format;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;

import com.facebook.swift.codec.ThriftEnumValue;
import com.gitee.l0km.codegen.annotations.CodegenEnumComment;
import com.gitee.l0km.codegen.annotations.CodegenEnumValue;
import com.gitee.l0km.com4j.basex.doc.ClassCommentProvider;
import com.gitee.l0km.com4j.basex.doc.ClassCommentProviderFactory;
import com.gitee.l0km.javadocreader.ClassCommentProviderFactoryImpl;
import com.google.common.base.Preconditions;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;


public class EnumMetadata<T extends Enum<T>>
{
	private final ClassCommentProviderFactory javadocCommentProviderFactory;
	/**
	 * {@link ClassCommentProvider}工厂接口实例
	 * 使用javadoc从源码读取的注释作为文档数据
	 */
	private final ClassCommentProvider javadocCommentProvider;
    private final Class<T> enumClass;
    private final Map<Integer, T> byEnumValue;
    private final Map<T, Integer> byEnumConstant;
    private final String enumName;
    private final ImmutableList<String> documentation;
    private final ImmutableMap<T, ImmutableList<String>> elementDocs;
    public EnumMetadata(
            String enumName,
            Class<T> enumClass)
            throws RuntimeException
    {
        checkNotNull(enumName, "enumName must not be null");
        checkNotNull(enumClass, "enumClass must not be null");
        this.enumName = enumName;
        this.enumClass = enumClass;
        javadocCommentProviderFactory = new ClassCommentProviderFactoryImpl();
        javadocCommentProvider =	javadocCommentProviderFactory.apply(getEnumClass());
        Method enumValueMethod = null;
        Method enumCommentMethod = null;
        for (Method method : enumClass.getMethods()) {
            if(null != checkAnnotation(method,CodegenEnumValue.class,int.class,Integer.class)){
            	enumValueMethod = method;
            }
            if(null != checkAnnotation(method,ThriftEnumValue.class,int.class,Integer.class)){
            	enumValueMethod = method;
            }
            if(null != checkAnnotation(method,CodegenEnumComment.class,String.class)){
            	enumCommentMethod = method;
            }
        }

        ImmutableMap.Builder<T, ImmutableList<String>> elementDocs = ImmutableMap.builder();
        if (enumValueMethod != null) {
            ImmutableMap.Builder<Integer, T> byEnumValue = ImmutableMap.builder();
            ImmutableMap.Builder<T, Integer> byEnumConstant = ImmutableMap.builder();
            for (T enumConstant : enumClass.getEnumConstants()) {
                Integer value;
                try {
                    value = (Integer) enumValueMethod.invoke(enumConstant);
                }
                catch (Exception e) {
                    throw new RuntimeException(format("Enum class %s element %s get value method threw an exception", enumClass.getName(), enumConstant), e);
                }
                Preconditions.checkArgument(
                        value != null,
                        "Enum class %s element %s returned null for enum value: %s",
                        enumClass.getName(),
                        enumConstant
                );

                byEnumValue.put(value, enumConstant);
                byEnumConstant.put(enumConstant, value);
                if(enumCommentMethod != null){                	
                	elementDocs.put(enumConstant, getCommentOfField(enumCommentMethod, enumConstant));
                }else{
                	elementDocs.put(enumConstant, ImmutableList.copyOf(javadocCommentProvider.commentOfField(enumConstant.name())));
                }
            }
            this.byEnumValue = byEnumValue.build();
            this.byEnumConstant = byEnumConstant.build();
        }
        else {
            byEnumValue = null;
            byEnumConstant = null;            
            for (T enumConstant : enumClass.getEnumConstants()) {
                if(enumCommentMethod != null){                	
                	elementDocs.put(enumConstant, getCommentOfField(enumCommentMethod, enumConstant));
                }else{
                	elementDocs.put(enumConstant, ImmutableList.copyOf(javadocCommentProvider.commentOfField(enumConstant.name())));
                }
            }
        }
        this.elementDocs = elementDocs.build();
        documentation = ImmutableList.copyOf(javadocCommentProvider.commentOfClass());
    }

    private ImmutableList<String> getCommentOfField(Method enumCommentMethod,T value){
    	try {
			String comments = (String) enumCommentMethod.invoke(value);
			if(!Strings.isNullOrEmpty(comments)){
				return ImmutableList.copyOf(comments.split("[\\r\\n]+"));
			}
			return ImmutableList.of();
		} catch (Throwable e) {
			Throwables.throwIfUnchecked(e);
			throw new RuntimeException(e);
		}
    }
    private Method checkAnnotation(Method method,Class<? extends Annotation> annClass,Class<?>...returnTypes){
        if (method.isAnnotationPresent(annClass)) {
            checkArgument(
                    Modifier.isPublic(method.getModifiers()),
                    "Enum class %s @%s method is not public: %s",
                    enumClass.getName(),
                    annClass.getSimpleName(),
                    method);
            checkArgument(
                    !Modifier.isStatic(method.getModifiers()),
                    "Enum class %s @%s method is static: %s",
                    enumClass.getName(),
                    annClass.getSimpleName(),
                    method);
            checkArgument(
                    method.getTypeParameters().length == 0,
                    "Enum class %s @%s method has parameters: %s",
                    enumClass.getName(),
                    annClass.getSimpleName(),
                    method);
            Class<?> returnType = method.getReturnType();
            for(Class<?> clazz:returnTypes){
            	if(returnType.isAssignableFrom(clazz)){
            		return method;
            	}
            }
            throw new IllegalArgumentException(String.format("Enum class %s @%s method does not return int or Integer: %s", enumClass.getName(),
                    annClass.getSimpleName(),
                    method));
        }
        return null;
    }
    public String getEnumName()
    {
        return enumName;
    }

    public Class<T> getEnumClass()
    {
        return enumClass;
    }

    public boolean hasExplicitThriftValue()
    {
        return byEnumValue != null;
    }

    public Map<Integer, T> getByEnumValue()
    {
        return byEnumValue;
    }

    public Map<T, Integer> getByEnumConstant()
    {
        return byEnumConstant;
    }

    public ImmutableList<String> getDocumentation()
    {
        return documentation;
    }

    public Map<T, ImmutableList<String>> getElementsDocumentation()
    {
        return elementDocs;
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        final EnumMetadata<?> that = (EnumMetadata<?>) o;

        if (!enumClass.equals(that.enumClass)) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode()
    {
        return enumClass.hashCode();
    }

    @Override
    public String toString()
    {
        final StringBuilder sb = new StringBuilder();
        sb.append("EnumMetadata");
        sb.append("{enumClass=").append(enumClass);
        sb.append(", byThriftValue=").append(byEnumValue);
        sb.append('}');
        return sb.toString();
    }
}
